/*
 * Excerpt.java
 *
 * Copyright (c) 2010 Jaymie Strecker (strecker at cs.umd.edu). All rights reserved.
 *
 * This file is part of Tonic.
 *
 * Tonic is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Tonic is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Tonic.  If not, see <http://www.gnu.org/licenses/>.
 */

package tonic.audio;

import tonic.error.InternalException;
import java.util.*;

/**
 * An Excerpt represents a segment of music to be analyzed as a unit.
 * It should contain just one chord or melodic note. It should have
 * about the same loudness throughout.
 *
 * After the ranked pitches have been calculated or given, the class becomes immutable.
 * Subsequent calls to analyzeSpectrum and analyzePitches have no effect. 
 *
 * @author Jaymie Strecker (strecker at cs.umd.edu)
 */
public class Excerpt {

    private final static double RECOMMENDED_MIN_SECONDS = 0.184;  // enough time for 3 cycles of C0
    private AudioData audioData;
    private int startIndex,  endIndex;  // startIndex inclusive, endIndex exclusive
    private float[] frequencies = null; // in ascending order from min. to max. pitch; length = endIndex - startIndex
    private double[] amplitudes = null; // in order corresp. to frequencies; all values are >= 0
    private Vector<PitchAmplitude> rankedPitches = null;    // in descending order of amplitude; pitch represents bin centered around pitch; amplitude represents average amplitude for that bin
    private boolean analyzed;

    // Recognizes pitches from C0 through B7
    private static final int MAX_OCTAVE = 7;
    private static final int MAX_SEMITONE = 11;

    @Override
    public String toString() {
        return "Audio data=" + audioData + ", " + startIndex + " to " + endIndex;
    }

    public int getStartSampleNum() {
        return startIndex;
    }

    public int getEndSampleNum() {
        return endIndex;
    }

    public Vector<PitchAmplitude> getRankedPitches() {
        if (rankedPitches == null) {
            return null; 
        } else {
            return new Vector<PitchAmplitude>(rankedPitches);
        }
    }

    protected Excerpt(AudioData audioData, int startIndex, int endIndex)
            throws IllegalArgumentException {
        if (!(0 <= startIndex && startIndex < audioData.getNumSamples()) ||
                !(0 < endIndex && endIndex <= audioData.getNumSamples()) ||
                !(startIndex < endIndex)) {
            throw new IllegalArgumentException("startIndex (" + startIndex +
                    ") or endIndex (" + endIndex + ") invalid");
        }
        this.audioData = audioData;
        this.startIndex = startIndex;
        this.endIndex = endIndex;
        analyzed = false; 

        double seconds = (endIndex - startIndex) * audioData.getSamplesPerSecond();
        if (seconds < RECOMMENDED_MIN_SECONDS) {
            System.out.println("WARNING: Excerpt duration (" + seconds +
                    " sec.) < recommended duration (" + RECOMMENDED_MIN_SECONDS + " sec.)");
        }
    }

    protected Excerpt(int startIndex, int endIndex, Vector<PitchAmplitude> rankedPitches) {
        this.startIndex = startIndex;
        this.endIndex = endIndex;
        this.rankedPitches = new Vector<PitchAmplitude>(rankedPitches);
        analyzed = true; 
    }

    // Pads array of samples with 0s to make (endIndex - startIndex + pad) a power of 2.
    // FFT throws exception if it's not a power of 2. 
    protected void analyzeSpectrum() {
        if (! analyzed) {
            int origNumSamples = endIndex - startIndex;
            int pow2NumSamples = Util.nextPowerOf2(origNumSamples);

            float[] samples = new float[pow2NumSamples];
            Arrays.fill(samples, 0);
            audioData.fillWithSamples(samples, startIndex, endIndex);
            FFT fft = new FFT(pow2NumSamples);
            amplitudes = fft.fftOfReals(samples);

            frequencies = new float[amplitudes.length];
            for (int i = 0; i < frequencies.length; i++) {
                frequencies[i] = i * audioData.getSamplesPerSecond() / pow2NumSamples;
            }
            // maxFreq == (numSamples/2 - 1) * samplesPerSecond / numSamples
            //         ~= samplesPerSecond/2
        }
    }

    // Pre: analyzeSpectrum has been called. 
    protected void analyzePitches() throws InternalException {
        if (! analyzed) {
            int numPitches = (MAX_OCTAVE + 1) * (MAX_SEMITONE + 1);
            rankedPitches = new Vector<PitchAmplitude>(numPitches);
            double[] averageAmplitudes = new double[numPitches];

            for (int octave = 0; octave <= MAX_OCTAVE; octave++) {
                for (int semitone = 0; semitone <= MAX_SEMITONE; semitone++) {

                    Pitch pitch;
                    try {
                        pitch = new Pitch(octave, semitone);
                    } catch (IllegalArgumentException e) {
                        throw new InternalException(
                                "Invalid pitch in ExcerptPiece.analyzePitches.");
                    }

                    // Calculate frequency range of pitch
                    float lowerFreq = pitch.frequency(-1);  // inclusive
                    float upperFreq = pitch.frequency(1);   // inclusive

                    // Calculate average amplitude of frequencies in that range
                    int lowerIndex = Arrays.binarySearch(frequencies, lowerFreq);
                    if (lowerIndex < 0) {
                        lowerIndex = -lowerIndex - 1;
                    }
                    averageAmplitudes[pitch.number()] = 0;
                    int count = 0;
                    for (int i = lowerIndex; i < frequencies.length && frequencies[i] <= upperFreq;
                            i++, count++) {
                        averageAmplitudes[pitch.number()] += amplitudes[i];
                    }
                    if (count > 0) {
                        averageAmplitudes[pitch.number()] /= count;
                    }

                    // Invariant: rankedPitches is in ascending order of pitch
                    rankedPitches.add(new PitchAmplitude(pitch, averageAmplitudes[pitch.
                            number()]));
                }
            }

            // Sort rankedPitches in descending order of amplitude
            Collections.sort(rankedPitches, new Comparator<PitchAmplitude>() {

                public int compare(PitchAmplitude p1, PitchAmplitude p2) {
                    return -(new Double(p1.amplitude).compareTo(new Double(p2.amplitude)));
                }
            });
            
            analyzed = true;
        }
    }

}
